Content & Deliverables
1) Community Project - Disaster Response Mapping
The aim of this project is to create an interactive map that is readily operable and accessible for Mudge Island residents and volunteers, particularly non-GIS users, in both online and offline circumstances. I set up a free QField account that people can use to update and review the map on their computers or phones anytime, anywhere. Map users can click to access the information they need, such as the precise location of water caches and evacuation points by boat or helicopter. This interactive map has proven invaluable in wildfire response decision-making and enhancing familiarity with the island’s environment. The map below is just a demo to show the map’s potential.
1) Capstone Project - Fuel type Mapping
Preliminary Fuel Type Map
In this project, I used a 15-band composite derived from 12 Sentinel-2 bands and 3 vegetation indices (NDVI, EVI, and NDWI) as a predictor and integrated four reference land cover maps (CCRS, CFS, VRI, and AAFC) into a high-confidence ground-truth map, where each class is agreed upon by at least three out of four sources. The one-directional convolutional neural network (1D-CNN) identifies each pixel by looking at its unique “color signature” across 15 layers of light and calculating a percentage score for each category; it then assigns the pixel to the class that gets the highest percentage, representing its best and most confident guess.
Code Snippets
library(terra)
# Compute per-pixel agreement count (1–4)
agree_count <- app(ref_stack, fun = function(x) {
if (all(is.na(x))) return(NA)
tab <- table(x)
return(max(tab))
})
# Compute modal (most common) class value
agree_modal <- app(ref_stack, fun = function(x) {
if (all(is.na(x))) return(NA)
tab <- table(x)
return(as.numeric(names(which.max(tab))))
})
import numpy as np
def compute_agreement(ref_stack):
bands, H, W = ref_stack.shape
flat = ref_stack.reshape(bands, -1)
agree_count = np.full(flat.shape[1], np.nan)
agree_modal = np.full(flat.shape[1], np.nan)
for i in range(flat.shape[1]):
x = flat[:, i]
x = x[~np.isnan(x)]
if x.size == 0:
continue
vals, cnts = np.unique(x, return_counts=True)
j = cnts.argmax()
# Compute per-pixel agreement count (1–4)
agree_count[i] = cnts[j]
# Compute modal (most common) class value
agree_modal[i] = vals[j]
return agree_count.reshape(H, W), agree_modal.reshape(H, W)
Biophysical Inputs: Biomass & Dryness indices
I also integrated two biophysical features into my preliminary fuel type map. The above-ground tree biomass map was reclassified into three classes: low (0–150 tons/ha), medium (150–350 tons/ha), and high (> 350 tons/ha) biomass. Sentinel-2 spectral bands (B03, BO8), topographic data (DEM, Slope), and PRISM climatic variables (MAT, MAP) were utilized to derive the dryness index and then reclassified into three fuel moisture classes: moist (0.00–0.30), moderate (0.30–0.60), and dry (0.60–1.00). Subsequently, I combined these two maps into a 9-class Biomass-Dryness (BD) Map.
Code Snippets
# Combine into Dryness Index (weights sum ≈ 1)
dryness <- (0.30 * mat_n) +
(0.25 * slope_n) +
(0.20 * map_n) +
(0.15 * ndwi_n) +
(0.10 * dem_n)
# Normalize to 0–1
dryness <- normalize(dryness)
import numpy as np
# Combine into Dryness Index (same weights as R)
dryness = (
0.30 * mat_n +
0.25 * slope_n +
0.20 * map_n +
0.15 * ndwi_n +
0.10 * dem_n
)
# Min–max normalization (terra::normalize equivalent)
min_val = np.nanmin(dryness)
max_val = np.nanmax(dryness)
dryness = (dryness - min_val) / (max_val - min_val)
Refined Fuel Model Map
The final fuel type map was generated by first filtering out unburnable areas, such as water and urban surfaces. Next, I employed CNN-based unmixing to identify specific vegetation patterns (e.g., Conifer (CF) & Broadleaf (BL)) and “mixed” classes (e.g., Grass-Shrub (GS) & Timber-Shrub-Grass (TSG)). This vegetation data was combined with a BD9 map, which classifies the landscape into nine categories based on biomass levels and dryness. Eventually, the decision matrix cross-references these two layers through a lookup table to assign each pixel a standardized fire behavior code from the Scott and Burgan (2005) system.
Code Snippets
# Build Lookup table
lookup <- list(
BL = c("Dry-Low"=fuel_ids$TL2, "Dry-Medium"=fuel_ids$TL6, "Dry-High"=fuel_ids$TL9, "Moderate-Low"=fuel_ids$TL2, "Moderate-Medium"=fuel_ids$TL6, "Moderate-High"=fuel_ids$TL9, "Moist-Low"=fuel_ids$TL2, "Moist-Medium"=fuel_ids$TL6, "Moist-High"=fuel_ids$TL9),
CF = c("Dry-Low"=fuel_ids$TL1, "Dry-Medium"=fuel_ids$TL3, "Dry-High"=fuel_ids$TL5, "Moderate-Low"=fuel_ids$TL1, "Moderate-Medium"=fuel_ids$TL3, "Moderate-High"=fuel_ids$TL5, "Moist-Low"=fuel_ids$TL1, "Moist-Medium"=fuel_ids$TL3, "Moist-High"=fuel_ids$TL5),
SH = c("Dry-Low"=fuel_ids$SH2, "Dry-Medium"=fuel_ids$SH5, "Dry-High"=fuel_ids$SH7, "Moderate-Low"=fuel_ids$SH6, "Moderate-Medium"=fuel_ids$SH3, "Moderate-High"=fuel_ids$SH9, "Moist-Low"=fuel_ids$SH6, "Moist-Medium"=fuel_ids$SH3, "Moist-High"=fuel_ids$SH9),
...
# Build Lookup table
lookup = {
"BL": {"Dry-Low": fuel_ids["TL2"], "Dry-Medium": fuel_ids["TL6"], "Dry-High": fuel_ids["TL9"], "Moderate-Low": fuel_ids["TL2"], "Moderate-Medium": fuel_ids["TL6"], "Moderate-High": fuel_ids["TL9"], "Moist-Low": fuel_ids["TL2"], "Moist-Medium": fuel_ids["TL6"], "Moist-High": fuel_ids["TL9"]},
"CF": {"Dry-Low": fuel_ids["TL1"], "Dry-Medium": fuel_ids["TL3"], "Dry-High": fuel_ids["TL5"], "Moderate-Low": fuel_ids["TL1"], "Moderate-Medium": fuel_ids["TL3"], "Moderate-High": fuel_ids["TL5"], "Moist-Low": fuel_ids["TL1"], "Moist-Medium": fuel_ids["TL3"], "Moist-High": fuel_ids["TL5"]},
"SH": {"Dry-Low": fuel_ids["SH2"], "Dry-Medium": fuel_ids["SH5"], "Dry-High": fuel_ids["SH7"], "Moderate-Low": fuel_ids["SH6"], "Moderate-Medium": fuel_ids["SH3"], "Moderate-High": fuel_ids["SH9"], "Moist-Low": fuel_ids["SH6"], "Moist-Medium": fuel_ids["SH3"], "Moist-High": fuel_ids["SH9"]},
...
3) Graduate Coursework Project - City Suitability Mapping
This interactive map allows users to locate the best places to live or invest in Metro Vancouver. You can easily adjust 16 different lifestyle and environmental factors, such as distance to CBD, rent costs, school proximity, and air quality, based on your personal priorities. By moving the sliders, you can generate a personalized Suitability Index that instantly recolors the map to highlight top-performing neighborhoods. Meanwhile, you may indicate your preference regarding certain contentious factors—those which have always been subject to differing opinions. The final result is a ranked list of the top 10 areas that most closely match your ideal balance of benefits and costs.